﻿/***************************************************COPYRIGHT***************************************
* Copyright (c) 2008, Whiteboard-IT
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that 
* the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following 
 * disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
 * disclaimer in the documentation and/or other materials provided with the distribution.
*
* Neither the name of Whiteboard-IT, LLC nor the names of its contributors may be used to endorse or promote 
 * products derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
*****************************************************************************************************************/
using System.Collections.Generic;
using System.Data;
using System.Linq;

namespace Mite
{
	/// <summary>
	/// This class is for generic validation.  
	/// These may differ in different databases so feel free to inherit and override these methods
	/// for implementations that vary
	/// </summary>
	public class AlterationValidation
	{
		/// <summary>
		/// 
		/// </summary>
		/// <param name="oc">Old Column</param>
		/// <param name="nc">New Column</param>
		/// <returns></returns>
		public virtual MigrationMessage[] GetColumnTransformationMessages(Column oc, Column nc)
		{
			var mesgs = new List<MigrationMessage>();
			mesgs.AddRange(GetValidateColumnMessages(nc));


			if (oc.IsNullable &&
			    !nc.IsNullable)
			{
				//TODO:  Make sure that there is a default value supplied
			}
			//check for always known unalter tables
			if (!IsUnalterableOneway(oc, nc))
			{
			}
			//is it a legal type transition
			if (!(IsTypeTransformationLegalOneway(oc.DataType, nc.DataType) && IsLegalBothways(oc.DataType, nc.DataType)))
			{
				var mesg = new MigrationMessage(nc.Table, oc);
				mesg.IsFatal = true;
				mesg.Message =
					string.Format(
						@"Datatype '{0}' => '{1}' is an illegal transformation.  Please use another type for your new column.",
						oc.DataType, nc.DataType);
				mesgs.Add(mesg);
			}
			if (!IsPrecisionLossPossible(oc, nc))
			{
				var mesg = new MigrationMessage(nc.Table, oc);
				mesg.IsFatal = false;
				mesg.Message = string.Format(@"'{0}': Precision loss may occur translating from {1}=>{2} ", nc.Name,
				                             oc.DataTypeString(), nc.DataTypeString());
			}


			IEnumerable<MigrationMessage> result = mesgs.Where(m => m != null);
			return result.Count() > 0 ? result.ToArray() : null;
		}

		protected virtual bool IsUnalterableOneway(Column oc, Column nc)
		{
			//TODO: Implement
			return false;
		}

		protected virtual bool IsPrecisionLossPossible(Column oc, Column nc)
		{
			if (oc.DataType ==
			    nc.DataType)
				return (nc.Precision >= oc.Precision); //Can't reduce the precision on same datatype
			if (IsStringDbType(nc.DataType))
			{
				if (oc.DataType ==
				    DbType.Binary)
					return nc.Precision >= oc.Precision;
				if (oc.DataType ==
				    DbType.Byte)
					return nc.Precision >= 3;
				if (oc.DataType ==
				    DbType.Boolean)
					return nc.Precision >= 1;
				if (oc.DataType ==
				    DbType.Currency)
					return nc.Precision >= 18;
				if (IsDateTimeType(oc.DataType))
					return nc.Precision >= 20; //Dec 12 2008 12:00AM
				if (oc.DataType ==
				    DbType.Decimal)
					return nc.Precision >= 30; //7.9 x 10^28 
				if (oc.DataType ==
				    DbType.Double)
				{
					return nc.Precision >= 337;
						//A floating point type representing values ranging from approximately 5.0 x 10 -324 to 1.7 x 10 308 with a precision of 15-16 digits.
				}
				if (oc.DataType ==
				    DbType.Guid)
					return nc.Precision >= 34;
				if (oc.DataType ==
				    DbType.Int16)
					return nc.Precision >= 6;
				if (oc.DataType ==
				    DbType.Int32)
					return nc.Precision >= 11;
				if (oc.DataType ==
				    DbType.Int64)
					return nc.Precision >= 20;
				if (oc.DataType ==
				    DbType.UInt16)
					return nc.Precision >= 6;
				if (oc.DataType ==
				    DbType.UInt32)
					return nc.Precision >= 10;
				if (oc.DataType ==
				    DbType.UInt64)
					return nc.Precision >= 20;
				if (oc.DataType == DbType.Xml &&
				    IsStringDbType(nc.DataType))
					return (nc.Precision >= oc.Precision);
			}


			return true; //TODO: Add additional illegalities
		}

		protected virtual bool IsDateTimeType(DbType input)
		{
			return input == DbType.Date || input == DbType.DateTime || input == DbType.DateTime2 ||
			       input == DbType.DateTimeOffset;
		}

		private static bool IsStringDbType(DbType input)
		{
			return (input == DbType.StringFixedLength
			        || input == DbType.String
			        || input == DbType.AnsiStringFixedLength
			        || input == DbType.AnsiString);
		}


		protected virtual bool IsLegalBothways(DbType col1, DbType col2)
		{
			var dic = new Dictionary<DbType, DbType>();

			dic.Add(DbType.StringFixedLength, DbType.String);
			return ((dic.ContainsKey(col1) && dic[col1] == col2)) || ((dic.ContainsKey(col2) && dic[col2] == col1));
		}

		protected virtual bool IsTypeTransformationLegalOneway(DbType from, DbType to)
		{
			var isLegalOneway = new List<KeyValuePair<DbType, DbType>>();
			isLegalOneway.Add(new KeyValuePair<DbType, DbType>(DbType.Int16, DbType.Int32));
			isLegalOneway.Add(new KeyValuePair<DbType, DbType>(DbType.Int32, DbType.Int64));
			isLegalOneway.Add(new KeyValuePair<DbType, DbType>(DbType.UInt16, DbType.UInt32));
			isLegalOneway.Add(new KeyValuePair<DbType, DbType>(DbType.UInt32, DbType.UInt64));
			isLegalOneway.Add(new KeyValuePair<DbType, DbType>(DbType.UInt16, DbType.UInt64));
			//Check Ints and UInts
			isLegalOneway.Add(new KeyValuePair<DbType, DbType>(DbType.UInt16, DbType.Int16));
			isLegalOneway.Add(new KeyValuePair<DbType, DbType>(DbType.UInt16, DbType.Int32));
			isLegalOneway.Add(new KeyValuePair<DbType, DbType>(DbType.UInt16, DbType.Int64));
			isLegalOneway.Add(new KeyValuePair<DbType, DbType>(DbType.UInt32, DbType.Int16));
			isLegalOneway.Add(new KeyValuePair<DbType, DbType>(DbType.UInt32, DbType.Int32));
			isLegalOneway.Add(new KeyValuePair<DbType, DbType>(DbType.UInt32, DbType.Int64));
			isLegalOneway.Add(new KeyValuePair<DbType, DbType>(DbType.UInt64, DbType.Int16));
			isLegalOneway.Add(new KeyValuePair<DbType, DbType>(DbType.UInt64, DbType.Int32));
			isLegalOneway.Add(new KeyValuePair<DbType, DbType>(DbType.UInt64, DbType.Int64));
			return (isLegalOneway.Contains(new KeyValuePair<DbType, DbType>(from, to)));
		}

		public virtual MigrationMessage[] GetValidateColumnMessages(Column col)
		{
			MigrationMessage[] mesgs = {GetIdentityMessage(col)};
			return mesgs;
		}


		/// <summary>
		/// This might need to be removed and handled at runtime. 
		/// A scenario is possible where both the drop of the key as well as the column would happen in the same 
		/// alteration and this would be legal.   Order of operations would have to be taken into account for 
		/// tihs to work in this instance and for now I believe that is out of scope.
		/// </summary> 
		/// <param name="col"></param>
		/// <param name="table"></param>
		/// <returns></returns>
		public virtual MigrationMessage GetColumnRemovalMessages(Column col, Table table)
		{
			//if any of these conditions are met then it's not legal
			//*  A replicated column.
			//* Used in an index.
			//* Used in a CHECK, FOREIGN KEY, UNIQUE, or PRIMARY KEY constraint.
			//TODO: Implement default and rules.  Are these MsSql specific should they be moved?
			//* Associated with a default defined with the DEFAULT keyword, or bound to a default object. 
			//* Bound to a rule. <-- not implemented

			if (col.IsIdentity || col.IsPrimaryKey || col.IsForeignKey || col.IsReplicated ||
			    col.IsUnique)
			{
				string mesg =
					string.Format(@"'{0} cannot be removed because it has a key index or rule associated with it.", col.Name);
				return new MigrationMessage(table, col, mesg);
			}
			else
				return null;
		}

		/// <summary>
		/// If valid identity column it returns null
		/// </summary>
		/// <param name="col">Column to validate</param>
		/// <returns></returns>
		protected virtual MigrationMessage GetIdentityMessage(Column col)
		{
			DbType[] legalTypes = {
			                      	DbType.Int16,
			                      	DbType.Int32,
			                      	DbType.UInt16,
			                      	DbType.UInt32,
			                      	DbType.VarNumeric,
			                      	DbType.Decimal
			                      };
			if (col.IsIdentity &&
			    !legalTypes.Contains(col.DataType))
			{
				var mesg = new MigrationMessage();
				mesg.IsFatal = true;
				mesg.SourceColumn = col;
				mesg.SourceTable = col.Table;
				mesg.Message = string.Format(@"'{0}': {1} is not valid for an identity ", col.Name, col.DataType);
				return mesg;
			}
			if (col.IsIdentity &&
			    col.Table.Columns.Contains(c => c.IsIdentity))
			{
				var mesg = new MigrationMessage();
				mesg.IsFatal = true;
				mesg.SourceColumn = col;
				mesg.SourceTable = col.Table;
				mesg.Message = string.Format(@"'{0}': there may only be on identity per table", col.Name);
				return mesg;
			}
			return null;
		}


		/// <summary>
		/// Returns null if no errors are found.
		/// </summary>
		/// <param name="alterations">Alterations to validate</param>
		/// <returns></returns>
		public virtual MigrationMessage[] GetTransformationErrors(params TableAlteration[] alterations)
		{
			var errors = new List<MigrationMessage>();
			foreach (TableAlteration alteration in alterations)
			{
				alteration.ColumnsToAdd.ForEach(col => errors.AddRange(GetColumnAdditionMessages(col, col.Table)));
				alteration.ColumnsToRemove.ForEach(col => errors.Add(GetColumnRemovalMessages(col, col.Table)));
			}
			return errors.ToArray();
		}

		public virtual MigrationMessage[] GetColumnAdditionMessages(Column col, Table table)
		{
			return GetValidateColumnMessages(col);
		}

		/// <summary>
		/// In order to get the Transformation Errors use GetTransformation Errors.
		/// </summary>
		public bool IsTransformationValid(params TableAlteration[] alterations)
		{
			return (GetTransformationErrors().Where(t => t.IsFatal) == null);
		}
	}

	public class MigrationMessage
	{
		public MigrationMessage(Table sourceTable, Column sourceColumn, string message)
		{
			SourceTable = sourceTable;
			SourceColumn = sourceColumn;
			Message = message;
		}

		public MigrationMessage(Table sourceTable, Column sourceColumn)
			: this(sourceTable, sourceColumn, string.Empty)
		{
		}

		public MigrationMessage()
		{
		}

		public bool IsFatal;
		public string Message;
		public Column SourceColumn;
		public Table SourceTable;
	}

	public class Warning: MigrationMessage
	{
		public Warning(Table table, Column column, string message): base(table, column, message)
		{
		}
	}
}